# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import random
import ccxt
import json
import os.path
import re
import time
import pickle
import traceback
import requests
import pandas as pd
from DrissionPage import SessionPage, SessionOptions
from api.position import get_spot_position, bn_transfer_spot_to_u
from api.market import load_market, fetch_binance_ticker_data
from api.trade import place_order, get_twap_symbol_info_list
from utils.functions import get_account_info
from utils.dingding import send_wechat_work_msg
from utils.commons import retry_wrapper
from config import data_path, order_path, proxy, exchange_basic_config, account_config, error_webhook_url
import warnings
warnings.filterwarnings('ignore')

# 构建浏览器的配置
options = SessionOptions()
if proxy:
    options.set_proxies(http=proxy['http'], https=proxy['https'])
# 初始化一个page，方便后面操作
page = SessionPage(session_or_options=options, timeout=30)
# 公告详细的baseurl
baseurl = 'https://www.binance.com/en/support/announcement/'
# 保存下架币种信息的位置
de_list_path = os.path.join(data_path, 'delist.json')
# 监测下架公告时间间隔。默认：60s监测一次，设置太短会被BN封禁
monitor_time = 60
# 获取数据的公共交易所对象
common_exchange = ccxt.binance(dict(exchange_basic_config, **{
    'api': {
        'sapi': {
            'get': {
                'spot/delist-schedule': 10  # 当前版本的ccxt不支持新上线的接口，那么我就让它支持呗，看不懂源码，不建议自己改
            },
        }
    }
}))


def get_page_info():
    de_list = []
    # ===读取本地下架数据
    try:
        with open(de_list_path, 'r') as file:
            de_list_dict = json.load(file)
            if 'last_id_list' not in de_list_dict:
                de_list_dict['last_id_list'] = []
                del de_list_dict['last_id']
    except Exception as e:
        de_list_dict = {'last_id_list': [], 'list': []}

    try:
        # 构造请求数据
        params = (('catalogId', '161'), ('pageNo', '1'), ('pageSize', '50'),)
        # 请求公告链接
        response = requests.get('https://www.binance.com/bapi/composite/v1/public/cms/article/catalog/list/query',
                                params=params, timeout=10, proxies=proxy)
        response.raise_for_status()  # 等待网络请求成功
        # 判断请求是否成功
        if response.json()["success"]:
            data = response.json()["data"]["articles"]

            id_list = []
            # 遍历最新的公告数据
            for item in data:
                # 判断公告id是否超过最新的id
                if item['id'] in de_list_dict['last_id_list']:
                    continue

                # 判断是否是逐仓保证金交易，直接跳过
                if 'Isolated Margin' in item['title']:
                    continue
                # 杠杆代币
                if 'Leveraged Tokens' in item['title']:
                    continue
                # 质押代币
                if 'Staking' in item['title']:
                    continue

                # 标题含有 delist 可以直接解析币种信息
                if 'delist' in item['title'].lower():
                    # 匹配正则规则
                    matches = re.findall(r'\b[A-Z]+\b', item['title'].replace('/', '').replace('USDⓈ-M', ''))
                    print('标题：', matches)
                    de_list.extend(matches)
                else:  # 标题不含有 delist，需要通过公告内容解析币种信息
                    # 移除非字母数字字符，将空格替换为连字符，并转为小写
                    clean_title = re.sub(r'\W+', ' ', item['title']).strip().replace(' ', '-').lower()
                    # 构建详情地址
                    url = f"{baseurl}{clean_title}-{item['code']}"
                    try:
                        # 打开页面
                        page.get(url, timeout=30)
                        # 解析内容
                        ele = page.ele('#__APP_DATA')
                        # 转成json格式
                        json_obj = json.loads(ele.raw_text)
                        # 解析需要的内容
                        # body = json_obj['appState']['loader']['dataByRouteId']['d969']['articleDetail']['body']
                        routeId = json_obj['appState']['loader']['dataByRouteId']
                        for key in routeId:
                            if 'articleDetail' in routeId[key]:
                                body = routeId[key]['articleDetail']['body']
                                break
                        else:
                            continue
                    except:
                        print('当前url内容无法解析：', url)
                        continue
                    # 匹配正则规则
                    matches = re.findall(r'\b[A-Z]+\b', body.replace('/', '').replace('USDⓈ-M', ''))
                    print('文章解析：', matches)
                    de_list.extend(matches)
                id_list.append(item['id'])
            # 更新最新的下架公告id
            id_list = list(set(id_list) - set(de_list_dict['last_id_list']))
            de_list_dict['last_id_list'].extend(id_list)
    except:
        print('页面接口解析出错', traceback.format_exc())

    return de_list, de_list_dict


def get_api_info():
    api_de_list = []
    try:
        time.sleep(random.randint(5, 10))  # 为了缓解api权重限制，随机休息5-10s
        res = retry_wrapper(common_exchange.sapi_get_spot_delist_schedule, func_name='获取现货下架数据')
        if res is not None and len(res) > 0:
            [api_de_list.extend(_['symbols']) for _ in res]
            print('请求接口返回：', api_de_list)
    except:
        print('api 获取现货下架数据出错')

    return api_de_list


def timed_task():
    while True:
        try:
            # 获取网页接口数据
            page_de_list, de_list_dict = get_page_info()

            # 获取api接口数据
            api_de_list = get_api_info()

            # 整合两个接口的数据
            de_list = page_de_list + api_de_list

            # 数据整理 (这里直接一刀切，有些币种是现货，有些是汇率对，都直接追加USDT，币种名称显示比较奇怪)
            # 这里追加不会有太大问题，我们主要使用USDT交易，所以这是没有问题的
            de_list = [_.replace('USDT', '') + 'USDT' for _ in de_list]  # 去掉USDT，再追加USDT结尾。防止一些公告中币种添加USDT后缀
            de_list = [_.replace('BUSDUSDT', 'BUSD') for _ in de_list]  # 去掉BUSDUSDT，改成BUSD。24年BN将不支持BUSD

            # 筛选出新增的下架币种
            new_data = list(set(de_list) - set(de_list_dict['list']))

            # ===发现新公告币种下架，执行平仓下架币种功能
            if len(new_data) > 0:
                print(f'下架币种：{new_data}')
                # 卖掉持仓
                try:
                    time.sleep(random.randint(10, 60))  # 随机sleep 10-60s，避免大家一起下单，滑点太大
                    clear_delist_pos(new_data)
                except BaseException as e:
                    traceback.print_exc()
                    print('清仓失败', e)

                # 发送下架通知(首次启动会load很多数据，建议首次之后手动打开)
                # send_wechat_work_msg(f'下架币种：{new_data}', error_webhook_url)

                # ===更新数据
                # 追加新下架的币种
                de_list_dict['list'].extend(new_data)

                # ===保存下架数据
                with open(de_list_path, 'w') as file:
                    json.dump(de_list_dict, file)
            else:
                print('没有新公告出现')
        except Exception as e:
            traceback.print_exc()
            print(f'币安下架币种接口报错：{e}')

        print('=' * 20, f'当前循环结束，sleep {monitor_time}s 准备进入下一轮循环', '=' * 20)
        time.sleep(monitor_time)


def clear_delist_pos(delist):
    swap_symbol_list, swap_min_qty, swap_price_precision, swap_min_notional = load_market(common_exchange,
                                                                                          symbol_type='swap')
    # =加载现货交易对信息
    spot_symbol_list, spot_min_qty, spot_price_precision, spot_min_notional = load_market(common_exchange,
                                                                                          symbol_type='spot')
    # =====获取账户信息,并且筛选含有现货的账户
    update_account_info = get_account_info(account_config.copy(), is_only_spot_account=True, is_operate=False)
    # 判断是否没能成功读取任一账户
    if not len(update_account_info.keys()):  # 如果update_account_info数据为空，表示更新账户信息失败
        print(f'delist脚本 所有账户更新信息失败')
        return

        # =====遍历所有账号监测
    for account_name, account_info in update_account_info.items():
        # ===获取账号的配置
        account_info['account_name'] = account_name  # 账户信息里面，追加一下用户名称
        exchange = account_info['exchange']
        spot_position = account_info['spot_position']
        swap_position = account_info['swap_position']
        max_one_order_amount = account_info['max_one_order_amount']
        twap_interval = account_info['twap_interval']
        wechat_webhook_url = account_info['wechat_webhook_url']

        # ===判断当前是否有仓位
        if spot_position.empty and swap_position.empty:  # 当前没有仓位直接跳过
            print('当前没有任何【持仓】，跳过后续操作')
            continue

        # ===计算下单信息
        swap_position = swap_position[swap_position.index.isin(delist)]
        swap_position['实际下单量'] = swap_position['当前持仓量'] * -1  # 直接通过当前持仓进行计算，方向要去反
        swap_position['实际下单资金'] = swap_position['实际下单量'] * swap_position['当前标记价格']  # 计算实际下单资金，用于后续拆单
        swap_position['交易模式'] = '清仓'  # 设置交易模式
        swap_position = swap_position[abs(swap_position['实际下单量']) > 0]  # 保留实际下单量 > 0 的数据
        swap_position.reset_index(inplace=True)
        print('合约下单信息：\n', swap_position)

        spot_position = spot_position[spot_position.index.isin(delist)]
        spot_position['实际下单量'] = spot_position['当前持仓量'] * -1  # 直接通过当前持仓进行计算，方向要去反
        spot_position['实际下单资金'] = spot_position['实际下单量'] * spot_position['当前价格']  # 计算实际下单资金，用于后续拆单
        spot_position['交易模式'] = '清仓'  # 设置交易模式
        spot_position = spot_position[abs(spot_position['实际下单量']) > 0]  # 保留实际下单量 > 0 的数据
        spot_position.reset_index(inplace=True)
        print('现货下单信息：\n', spot_position)

        # 判断是否需要有下单信息
        if swap_position.empty and spot_position.empty:
            continue

        # ===使用twap算法拆分订单
        twap_swap_symbol_info_list = get_twap_symbol_info_list(swap_position, max_one_order_amount)
        # ===遍历下单
        for i in range(len(twap_swap_symbol_info_list)):
            # 获取币种的最新价格
            symbol_last_price = fetch_binance_ticker_data(exchange)
            # 逐批下单(拉黑之后，增加一点滑点下单，防止下单失败)
            place_order(exchange, twap_swap_symbol_info_list[i], symbol_last_price, swap_min_qty,
                        swap_price_precision, swap_min_notional, symbol_type='swap',
                        wechat_webhook_url=wechat_webhook_url, slip_rate=0.03)
            # 下单间隔
            print(f'等待 {twap_interval}s 后继续下单')
            time.sleep(twap_interval)

        # ===现货处理
        if not spot_position.empty:
            # ===使用twap算法拆分订单
            twap_spot_symbol_info_list = get_twap_symbol_info_list(spot_position, max_one_order_amount)
            # ===现货遍历下单
            for i in range(len(twap_spot_symbol_info_list)):
                # 获取现货币种的最新价格
                symbol_last_price = fetch_binance_ticker_data(exchange, symbol_type='spot')
                # 逐批下单(拉黑之后，增加一点滑点下单，防止下单失败)
                place_order(exchange, twap_spot_symbol_info_list[i], symbol_last_price, spot_min_qty,
                            spot_price_precision, spot_min_notional, symbol_type='spot',
                            wechat_webhook_url=wechat_webhook_url, slip_rate=0.03)
                # 下单间隔
                print(f'等待 {twap_interval}s 后继续下单')
                time.sleep(twap_interval)

            # ===将现货中的U转入的合约账号
            spot_position = get_spot_position(exchange)
            if 'USDT' in spot_position['symbol'].to_list():
                usdt = spot_position.loc[spot_position['symbol'] == 'USDT', '当前持仓量'].iloc[0]
                # 转资金到合约账户
                bn_transfer_spot_to_u(exchange, round(usdt - 1, 1))

        # 构建轮动存储选币目录
        file_path = os.path.join(data_path, f'{account_info["account_name"]}')
        if not os.path.exists(file_path):
            os.makedirs(file_path)
        file_path = os.path.join(file_path, 'shift_select_coin_result.pkl')
        # 判断是否需要更新本地选币文件
        if os.path.exists(file_path):  # 文件存在，说明需要更新最近的选币文件
            # 读取本地存储的文件
            with open(file_path, 'rb') as f:
                local_select_result = pickle.load(f)
            # 更新选币
            for factor in list(local_select_result.keys()):  # 因子
                for key in list(local_select_result[factor].keys()):  # offset
                    _select_coin = local_select_result[factor][key]['select_coin']
                    local_select_result[factor][key]['select_coin'] = _select_coin[~_select_coin['symbol'].isin(delist)]

            # 保存轮动选币文件
            with open(file_path, 'wb') as f:
                pickle.dump(local_select_result, f)

        # 判断非reb的order文件是否存在，如果存在则剔除拉黑的币种
        order_file = os.path.join(order_path, f'{account_name}_order.csv')
        if os.path.exists(order_file):
            local_order_df = pd.read_csv(order_file, encoding='gbk', parse_dates=['candle_begin_time', '换仓时间'])
            local_order_df = local_order_df[~local_order_df['symbol'].isin(delist)]
            local_order_df.reset_index(inplace=True, drop=True)
            local_order_df.to_csv(order_file, encoding='gbk', index=False)

        # ===发送信息推送
        send_wechat_work_msg(f'成功清仓下架币种，已将USDT转入U本位合约账户补充保证金', wechat_webhook_url)

        # ===休息一会儿
        time.sleep(2)


if __name__ == '__main__':
    try:
        timed_task()
    except KeyboardInterrupt:
        print('退出')
        exit()
    except BaseException as err:
        msg = '系统出错，10s之后重新运行，出错原因: ' + str(err)
        print(msg)
        send_wechat_work_msg(msg, error_webhook_url)
        traceback.print_exc()

